const EventEmitter = require('events')
const inquirer = require('inquirer')
const cloneDeep = require('lodash.clonedeep')

const {
  chalk,
  execa,

  log,

  hasGit,
  hasProjectGit,
  hasYarn,
  hasPnpm3OrLater,
  hasPnpmVersionOrLater,

  exit,
  loadModule
} = require('@vue/cli-shared-utils')
const writeFileTree = require('../utils/writeFileTree')
const { loadOptions } = require('./options')
const { loadPkgConfig } = require('../utils/pkg')
const sortObject = require('../utils/sortObject')
const generateReadme = require('../utils/generateReadme')

const PromptModuleAPI = require('./PromptModuleAPI')
const PackageManager = require('./ProjectPackageManager')
const Generator = require('./Generator')

// const isManualMode = (answers) => answers.preset === "__manual__"; //for manual isn't support now

class Creator extends EventEmitter {
  constructor(name, context, promptModules) {
    super()
    this.name = name
    this.context = process.env.VUE_CLI_CONTEXT = context
    const { appPrompt, featurePrompt } = this.resolveIntroPrompts()

    this.appPrompt = appPrompt
    this.featurePrompt = featurePrompt
    this.injectedPrompts = []
    this.promptCompleteCbs = []
    this.afterInvokeCbs = []
    this.afterAnyInvokeCbs = []

    this.run = this.run.bind(this)

    const promptAPI = new PromptModuleAPI(this)
    promptModules.forEach((m) => m(promptAPI))
  }

  async create(cliOptions = {}, preset = null) {
    const { run, name, context, afterInvokeCbs, afterAnyInvokeCbs } = this

    if (cliOptions.type) {
      // validate app type
      if (!['vue', 'react'].includes(cliOptions.type)) {
        console.error(chalk.red(`Invalid app type: "${cliOptions.type}"`))
        console.log(`Please use ${chalk.green('vue')} or ${chalk.green('react')}`)
      }

      // now is not support react to create project
      if (cliOptions.type === 'react') {
        console.warn(
          `${chalk.white.bgYellow(' warning ')}   Sorry! react is not support now, please wait for new version`
        )
      } else {
        // 处理优化后面再写
        console.warn(`${chalk.white.bgYellow(' warning ')}   Sorry! this way is not support now`)
      }
    } else {
      preset = await this.promptAndResolve()
    }

    // clone before mutating
    preset = cloneDeep(preset)
    // inject core service
    const pkg = await loadPkgConfig(name, preset)

    const packageManager =
      cliOptions.packageManager ||
      loadOptions().packageManager ||
      (hasYarn() ? 'yarn' : null) ||
      (hasPnpm3OrLater() ? 'pnpm' : 'npm')

    const pm = new PackageManager({
      context,
      forcePackageManager: packageManager
    })

    log(`✨  Creating project in ${chalk.yellow(context)}.`)
    this.emit('creation', { event: 'creating' })

    await writeFileTree(context, {
      'package.json': JSON.stringify(pkg, null, 2)
    })

    // generate a .npmrc file for pnpm, to persist the `shamefully-flatten` flag
    if (packageManager === 'pnpm') {
      const pnpmConfig = hasPnpmVersionOrLater('4.0.0')
        ? // pnpm v7 makes breaking change to set strict-peer-dependencies=true by default, which may cause some problems when installing
          'shamefully-hoist=true\nstrict-peer-dependencies=false\n'
        : 'shamefully-flatten=true\n'

      await writeFileTree(context, {
        '.npmrc': pnpmConfig
      })
    }

    // intilaize git repository before installing deps
    // so that vue-cli-service can setup git hooks.
    const shouldInitGit = this.shouldInitGit(cliOptions)
    if (shouldInitGit) {
      log()
      log(`🔥  Initializing git repository...`)
      this.emit('creation', { event: 'git-init' })
      await run('git init')
    }

    // install plugins
    log(`⚙\u{fe0f}  Installing CLI plugins. This might take a while...`)
    log()
    this.emit('creation', { event: 'plugins-install' })
    await pm.install()

    // run generator
    log(`🚀  Invoking generators...`)
    this.emit('creation', { event: 'invoking-generators' })
    const plugins = await this.resolvePlugins(preset.plugins, pkg)
    const generator = new Generator(context, {
      pkg,
      plugins,
      afterInvokeCbs,
      afterAnyInvokeCbs
    })

    await generator.generate({
      rootOptions: Object.assign({}, preset, { name })
    })

    // install additional deps (injected by generators)
    log(`📦  Installing additional dependencies...`)
    this.emit('creation', { event: 'deps-install' })
    log()
    await pm.install()

    // run complete cbs if any (injected by generators)
    log()
    log(`⚓  Running completion hooks...`)
    this.emit('creation', { event: 'completion-hooks' })
    for (const cb of afterInvokeCbs) {
      await cb()
    }
    for (const cb of afterAnyInvokeCbs) {
      await cb()
    }

    if (!generator.files['README.md']) {
      // generate README.md
      log()
      log('📄  Generating README.md...')
      await writeFileTree(context, {
        'README.md': generateReadme(generator.pkg, packageManager)
      })
    }

    if (preset.features.includes('githook')) {
      log()
      log('📚  Generating husky gitHooks...')
      await writeFileTree(context, {
        '.husky/pre-commit': `#!/bin/sh\n. "$(dirname "$0")/_/husky.sh"\n\n${packageManager} run lint-staged`,
        '.husky/commit-msg': '#!/bin/sh\n. "$(dirname "$0")/_/husky.sh"\n\nnpx --no-install commitlint --edit $1'
      })
    }

    log()
    log(`🎉  Successfully created project ${chalk.yellow(name)}.`)

    try {
      const commandProcess = run(`${packageManager} run start`)
      commandProcess.stdout.on('data', function (data) {
        let output = data.toString()

        const regex = /https?:\/\/[\w\-._~:/?#\[\]@!$&'()*+,;%=]+/gi
        let finalResult = output.replace(regex, (url) => chalk.green(url))

        if(preset.features.includes('githook')) {
          finalResult = finalResult.replace(/To create.*run build./, (text) => text + `\n  Run ${packageManager} run prepare when changing the .git.`)
        }
        process.stdout.write(finalResult)
      })
    } catch (error) {
      console.error(chalk.red(`执行命令时发生错误: ${error}`))
    }
  }

  // init appPrompt and next version
  resolveIntroPrompts() {
    const appChoices = [
      { name: 'vue', value: 'vue' },
      { name: 'react', value: 'react' }
    ]
    const appPrompt = {
      name: 'type',
      type: 'list',
      message: `Please pick a app type:`,
      choices: [...appChoices]
    }
    const featurePrompt = {
      name: 'features',
      when: (answers) => answers.type === 'vue' && 'vueVersion' in answers,
      type: 'checkbox',
      message: 'Check the features needed for your project:',
      choices: [],
      pageSize: 10
    }
    return {
      appPrompt,
      featurePrompt
    }
  }

  async promptAndResolve(answers = null) {
    if (!answers) {
      answers = await inquirer.prompt(this.resolveFinalPrompts())
    }

    let preset = {
      ...answers,
      plugins: {}
    }
    return preset
  }

  resolveFinalPrompts() {
    // patch generator-injected prompts to only show in manual mode
    this.injectedPrompts.forEach((prompt) => {
      const originalWhen = prompt.when || (() => true)
      prompt.when = (answers) => {
        return originalWhen(answers)
      }
    })

    const prompts = [this.appPrompt, ...this.injectedPrompts, this.featurePrompt]
    return prompts
  }

  run(command, args) {
    if (!args) {
      ;[command, ...args] = command.split(/\s+/)
    }
    return execa(command, args, { cwd: this.context })
  }

  shouldInitGit(cliOptions) {
    if (!hasGit()) {
      return false
    }
    // --git
    if (cliOptions.git) {
      return true
    }
    // --no-git
    if (cliOptions.git === false || cliOptions.git === 'false') {
      return false
    }
    // default: true unless already in a git repo
    return !hasProjectGit(this.context)
  }

  // { id: options } => [{ id, apply, options }]
  async resolvePlugins(rawPlugins, pkg) {
    // ensure cli-service is invoked first
    rawPlugins = sortObject(rawPlugins, ['@vue/cli-service'], true)
    const plugins = []
    for (const id of Object.keys(rawPlugins)) {
      const apply = loadModule(`${id}/generator`, this.context) || (() => {})
      let options = rawPlugins[id] || {}

      plugins.push({ id, apply, options })
    }
    return plugins
  }
}

module.exports = Creator
